hooks 进阶
在 React 中,useReducer、useMemo、useCallback 是用于优化性能和管理状态的重要的 Hook。
它们的底层原理都与 React 的渲染机制、依赖追踪、记忆化(memoization)策略密切相关
优先考虑代码的可读性,在性能瓶颈处针对性使用优化 Hook。过度使用 useMemo/useCallback 可能适得其反
一、底层原理
1. useReducer
useReducer 是一个状态管理的 Hook,它接收一个 reducer 函数和一个初始状态 initialState。返回一个当前状态和一个 dispatch 函数。
React 内部维护一个状态管理单元(类似于 Redux),储存在 Fiber 节点中 memoizedState 属性(以链表形式储存)的 (state, dispatch) 对,包含:
-
state:当前状态值 -
dispatch:触发更新的函数 -
reducer:纯函数(state, action) => newState -
初始化:在组件首次渲染时,
useReducer会调用reducer(initialState, undefined)来计算初始状态并存储 -
存储状态:React 内部为每个 Hook 创建一个
Hook对象,该对象包含memoizedState(存储当前状态)和queue(存储待处理的action队列) -
dispatch函数:返回的dispatch函数是一个闭包,它持有对当前Hook对象和reducer函数的引用。当dispatch(action)被调用时:- 将
action添加到Hook对象的queue中 - 触发组件的重新渲染(调度更新)
- 将
-
重新渲染时:在组件函数再次执行时,
useReducer会:- 从
Hook对象的queue中取出所有待处理的action - 按顺序应用
reducer 函数:newState = reducer(oldState, action) - 将最终计算出的状态作为当前状态返回,并清空
queue
- 从
dispatch 在组件生命周期内保持不变(依赖不变时),避免子组件无效重新渲染
function useReducer(reducer, initialState) {
// 从当前的 Fiber 的 memoizedState 中获取当前的 Hook(依赖调用顺序)
let hook = mountWorkInProgressHook();
// 储存当前的状态
hook.memoizedState = hook.baseState = initialState;
// 储存待处理的 action 列队
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState,
});
/** 是一个闭包,持有当前 `hook` 对象和 `reducer` 函数的引用 */
const dispatch = (queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue,
));
return [hook.memoizedState, dispatch];
}
2. useMemo
在 Fiber 节点中储存 {value, dependencies},并使用 Object.is 比较依赖数组。
- 储存:
Hook对象储存memoizedState(记忆化的值)和memoizedDeps(上一次的依赖项数组) - 首次渲染:执行传入的
factory函数,将结果储存在memoizedState中,并将依赖项deps储存在memoizedDeps中 - 更新渲染:比较当前传入的
deps数组与memoizedDeps数组中的每一项(使用Object.is进行浅比较)- 如果所有依赖项都没有变化,则直接返回
memoizedState(缓存的值) - 如果任何一个依赖项发生变化,则重新执行
factory函数,用新结果更新memoizedState,并用新的deps更新memoizedDeps
- 如果所有依赖项都没有变化,则直接返回
function useMemo(factory, deps) {
const hook = mountWOrkInProgressHook();
// 对比依赖项是否变化
if (depsChanged(hook.memoizedState?.deps, deps)) {
// 子发生变化时更新储存的状态
hook.memoizedState = {
value: factory(),
deps,
};
}
return hook.memoizedState.value;
}
3. useCallback
本质是 useMemo 的语法糖。
useCallback(fn, deps) === useMemo(() => fn, deps);
- 函数缓存:避免每次渲染创建新函数实例
- 依赖比对:依赖变化时返回新函数
二、 useReducer 与 useState
| 特性 | useReducer | useState |
|---|---|---|
| 状态更新 | 通过 dispatch(action) | 直接 setState(newValue) |
| 复杂状态 | ✅ 更适合多状态关联 | ⚠️ 易导致多次更新 |
| 逻辑分离 | reducer 抽离业务逻辑 | 更新逻辑内联在组件 |
| 中间状态 | ✅ 避免读写分离问题 | ❌ 可能产生中间态 |
| 性能优化 | ✅ 稳定 dispatch 引用 | 需手动优化 setState |
| 新能 | 引入了 reducer 函数的调用,但对于复杂的状态,逻辑更清晰,可能减少不必要的重渲染 | 对于简单的状态,性能较小 |
| 合适选择 | 复杂状态/多操作 | 简单状态 |
三、useCallback 与 useMemo
| 特性 | useCallback | useMemo |
|---|---|---|
| 缓存目标 | 函数 | 任意计算结果(可以是任意类型:数字、字符串、对象、数组、函数等) |
| 等价形式 | useMemo(() => fn, deps) | -- |
| 主要用途 | 稳定函数引用 | 避免重复计算 |
| 典型场景 | 传递给 memoized 子组件 | 记忆化昂贵计算 |
// useCallback 用法
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
// useMemo 用法
const sortedList = useMemo(() => {
return list.sort((a, b) => a - b);
}, [list]);
四、 useMemo 与 React.memo
| 特性 | useMemo | React.memo |
|---|---|---|
| 作用层级 | 组件内部的 Hook | 组件外包裹的高阶组件(HOC)或组件包装器 |
| 优化目标 | 避免内部重复计算 | 避免整个组件重渲染 |
| 依赖关系 | 控制具体值的缓存 | 浅比较 props |
| 配合使用 | 常与 memo 搭配稳定 props | 需稳定 props 输入 |
| 性能优化点 | 优化渲染过程中的计算开销 | 优化组件的渲染过程的本身(是否需要重新 render) |
| 使用场景 | 计算派生数据(如,过滤,排序);创建复杂的对或数组;记忆化回调函数(useCallback) | 子组件接收的 props 经常不变;子组件渲染开销大;防止父组件重渲染导致不必要的子组件重渲染 |
const Child = React.memo(({ onClick }) => {
// 避免子组件渲染
});
function Parent() {
const handleClick = useCallback(() => {}, []);
// useMemo 可优化传递的对象类型 prop
return <Child onClick={handleClick} />;
}
五、 一个简单的小例
const data = useMemo(() => computeExpensiveValue(a, b), [a, b]);
const update = useCallback(() => handler(data), [data]);
return <MemoizedComponent data={data} onClick={update} />;
六、useContext 与 useReducer 结合使用
二者结合的价值是:用 useReducer 管理复杂状态,用 useContext 共享状态和更新函数(dispatch)
- 用
useReducer集中管理状态和更新逻辑(代替使用分散的useState) - 用
useContext将状态和dispatch传递给多有需要的子组件,避免逐层传递props
// 1. 定义 reducer 管理主题状态
function themeReducer(state, action) {
switch (action.type) {
case 'TOGGLE_THEME':
return { ...state, isDark: !state.isDark };
default:
return state;
}
}
// 2. 创建 Context
const ThemeContext = createContext();
// 3. 父组件提供状态和 `dispatch`
function App() {
const [themeState, dispatch] = useReducer(themeReducer, { isDark: false });
return (
<ThemeContext.Provider value={{ themeState, dispatch }}>
<Header />
<Content />
</ThemeContext.Provider>
);
}
// 4. 深层子组件直接使用 `dispatch` 更新状态
function Header() {
const { themeState, dispatch } = useContext(ThemeContext);
return (
<button onClick={() => dispatch({ type: 'TOGGLE_THEME' })}>
{themeState.isDark ? '切换亮色模式' : '切换暗色模式'}
</button>
);
}
- 当多个状态存在依赖关系时(如表单验证),用
useReducer合并状态为一个对象,通过dispatch统一更新,避免因多个useState导致的多次渲染。结合useContext后,子组件只需订阅一次上下文,减少不必要的重渲染 reducer函数可以被多个组件或模块复用(如不同的页面共享同一状态更新逻辑),而useContext确保状态和更新函数的一致性